AWS CDK の Construct ID はどのように命名するべきか?

AWS CDK の Construct ID はどのように命名するべきか?

Construct ID命名に関するTipsを調べると色々とありますが、命名時の明確な方針が欲しかったのでまとめてみました
Clock Icon2024.09.27

こんにちは。リテールアプリ共創部のきんじょーです。

AWS CDK でインフラを定義する際、皆さんは Construct ID をどのようなルールで命名していますか?

Construct ID を適切に命名することで、自動生成されるリソース名や CloudFormation の 論理ID の可読性を高く保ち、保守しやすいインフラを構築できます。

以下のブログでは CDK と CloudFormation でデプロイされるリソースの命名規則について全体像が解説されています。

https://dev.classmethod.jp/articles/how-aws-cdk-determines-resource-names/

この記事では CDK による論理 ID 生成処理をさらに深掘りし、それを踏まえた上でどのように Construct ID を命名すべきか実装例を交えてご紹介します。

先に結論

以下を意識することで、自動生成されるリソース名と CloudFormation の 論理ID の可読性を高められます。

  • Construct ID は scope の中で一意にする
  • Construct ID はPascalCaseで記述する
  • Construct ID にConstructStackとつけない
  • 親 コンストラクト で表している情報を繰り返さない
  • 技術詳細を指定しすぎない
  • DefaultResource を適切に活用し Construct ID を短縮する
    • Default を使うべき場合
      • コンストラクトツリー のリファクタリングを行う場合
      • 再利用を考慮しない カスタムコンストラクト を定義し、将来的にリファクタリングの可能性がある場合
    • Resource を使う場合
      • 再利用可能な カスタムコンストラクト を作成する場合

なぜ Construct ID の命名にこだわる必要があるか

Construct ID の重要性を説明する前に、事前知識として Construct ID と 論理 ID について説明します。

Construct ID とは

Construct ID とは、CDK で コンストラクト を インスタンス化 する際に、第 2 引数に渡す文字列のことです。
CDK では コンストラクト を利用して Stack 内のリソースを構造化できます。指定する Construct ID は コンストラクト のスコープ内で一意である必要があります。

new sqs.Queue(this, "MyQueue"); // MyQueue が Construct ID

論理 ID とは

論理 IDは CloudFormation テンプレートの Resources セクションでリソースを一意に示す ID です。論理 ID の変更はリソースの置換を意味します。

Resources:
  MyEC2Instance: # 論理ID
    Type: AWS::EC2::Instance
    Properties:
      ImageId: ami-0ff8a91507f77f867

Construct ID と論理 ID の関係性

Construct ID は CDK から CloudFormation テンプレートを合成する際、CloudFormation の論理 ID を決めるために使用されます。

リソースに明示的に名前を指定しない場合、CloudFormation は論理 ID をもとにリソース名を生成します。図にすると以下の関係性です。

CloudFormation の論理 ID は 255 文字まで指定可能です。
しかし自動生成されるリソース名は論理 ID を一定の桁数で切り捨てるため、長すぎる論理 ID はリソース名を正しく表せないことがあります。

また論理 ID が冗長な場合、CloudFormation の TreeView や FlatView で Stack 内のリソース識別がしづらくなる問題もあります。

貼り付けた画像_2024_09_27_0_37.png

Construct ID を適切に命名することで、自動生成されるリソース名や CloudFormation の TreeView や FlatView の可読性を高めることに繋がります。

論理 ID からリソースの命名が決まる CloudFormation 側の仕組みは、前述のブログで詳細に解説されているので併せてそちらもご覧ください。

CDK の論理 ID 生成処理

CDK の論理 ID を生成する実装は以下にあります。

https://github.com/aws/aws-cdk/blob/4b00ffeb86b3ebb9a0190c2842bd36ebb4043f52/packages/aws-cdk-lib/core/lib/stack.ts#L1357-L1362

https://github.com/aws/aws-cdk/blob/4b00ffeb86b3ebb9a0190c2842bd36ebb4043f52/packages/aws-cdk-lib/core/lib/private/uniqueid.ts#L32-L71

特筆すべき点をかいつまんで説明します。

上位 コンストラクト の ID を連結する

命名対象の コンストラクトツリー を走査し、top-level コンストラクト(Stack 直下の コンストラクト)までの Construct ID を取得します。

protected allocateLogicalId(cfnElement: CfnElement): string {
 const scopes = cfnElement.node.scopes;
 const stackIndex = scopes.indexOf(cfnElement.stack);
 const pathComponents = scopes.slice(stackIndex + 1).map(x => x.node.id);
 return makeUniqueId(pathComponents);
}

たとえば以下のような Construct ID を振られた Tree があった場合、

生成される CloudFormation テンプレートで確認できるMetadata/aws:cdk:pathと、ID 生成に使用されるパスツリーは以下になります。

Construct ID Metadata/aws:cdk:path ID 生成に使用されるパスツリー
Api ServerStack/Api Api
ApiGateway ServerStack/Api/ApiGateway ApiApiGateway
Lambda ServerStack/Api/Lambda ApiLambda

Construct ID から除外される文字列

論理 ID 生成時に Construct ID に含まれる英数字以外の文字列は除去されます。

function removeNonAlphanumeric(s: string) {
  return s.replace(/[^A-Za-z0-9]/g, "");
}

Construct ID をkebab-caseで記述している場合、-は除外されてしまい、区切り文字が無くなってしまう点に注意が必要です。
上記の理由から、意図せぬ ID 重複を防いだり、論理 ID とリソース名の可読性を上げるため Construct ID にはPascalCaseが推奨されています。

https://qiita.com/tmokmss/items/721a99e9a62499d6d54a

論理 ID の Suffix にハッシュ値がつく仕組み

CloudFormation の 論理 ID を一意にするため、CDK では Construct ID のパスツリーを元に md5 のハッシュ値を生成し、先頭 8 文字を 論理 ID の suffix に付与します。

// ハッシュ値の生成処理
function pathHash(path: string[]): string {
  const md5 = md5hash(path.join(PATH_SEP));
  return md5.slice(0, HASH_LEN).toUpperCase();
}

// 人間が可読できるIDとハッシュ値の付与
const hash = pathHash(components);
const human = removeDupes(components)
  .filter((x) => x !== HIDDEN_FROM_HUMAN_ID)
  .map(removeNonAlphanumeric)
  .join("")
  .slice(0, MAX_HUMAN_LEN);

return human + hash;

これには例外があり、命名対象が top-level コンストラクト である場合は suffix がつきません。
これは、既存の CloudFormation テンプレートから CDK スタックに移行する際に、top-level コンストラクト として定義することで論理 ID の変更なしでの移行を可能にする目的です。

冗長な情報を 論理 ID から除去する仕組み

コンストラクト のパスツリーの最後で同じ文字列が繰り返されている場合、その文字列は除去されます。
具体的には以下のような実装をした場合です。

// SQSのカスタムコンストラクトを定義
export class QueueConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    new sqs.CfnQueue(this, "MyQueue");
  }
}

// SQSのカスタムコンストラクトを利用
export class SampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new QueueConstruct(this, "MyQueue");
  }
}

コンストラクトツリー は以下のようになり、パスツリーはSampleStack/MyQueue/MyQueueとなります。

通常であれば、論理 ID はMyQueueMyQueue+hashとなるはずですが、MyQueueの重複が除去され、MyQueue+hashとなります。
実際には以下のテンプレートが出力されました。

MyQueue4F9177CF:
  Type: AWS::SQS::Queue
  Metadata:
    aws:cdk:path: SampleStack/MyQueue/MyQueue

特殊な Construct ID (Default, Resource)

Construct ID にはDefaultResourceという特殊な ID を指定可能です。
どちらも論理 ID 生成時にパスツリーから除外されるという特徴を持っており、これらを適切に使用することで論理 ID が長くなることを防げます。

Construct ID のパスツリーは、論理 ID 生成時に 3 つの処理で使用されます。

  1. top-level コンストラクト かどうかの判定
  2. 論理 ID の人間が可読できるパートの生成
  3. suffix のハッシュ計算

DefaultResourceの違いは、Defaultは上記 1、2、3 すべてから除外され、Resourceは 2 からのみ除外される点です。

以下のような コンストラクト があったとします。

// 3つのSQSキューを定義するコンストラクト
export class QueuesConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    new sqs.CfnQueue(this, "MyQueue");
    new sqs.CfnQueue(this, "Default");
    new sqs.CfnQueue(this, "Resource");
  }
}

// 3つのSQSキューを定義するConstructを利用
export class SampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    new QueuesConstruct(this, "QueuesConstruct");
  }
}

この場合の コンストラクトツリー は以下のようになります。

それぞれの判定時のパスツリーの値と、最終的に生成される論理 ID を以下にまとめました。

Construct ID Metadata/aws:cdk:path 1. top-level コンストラクト 判定時 2. 可読部の生成時 3. ハッシュ計算時 論理 ID
MyQueue SampleStack/QueuesConstruct/MyQueue QueuesConstruct/MyQueue QueuesConstructMyQueue QueuesConstructMyQueue -> 56B5211F QueuesConstructMyQueue56B5211F
Default SampleStack/QueuesConstruct/Default QueuesConstruct QueuesConstruct top-level Construct 扱いで hash は付与されない QueuesConstruct
Resource SampleStack/QueuesConstruct/Resource QueuesConstruct/Resource QueuesConstruct QueuesConstruct/Resource -> D22CAD06 QueuesConstructD22CAD06

Defaultを利用した場合、top-level コンストラクト 扱いになり hash は付与されませんでした。
これはコンストラクトツリーの階層が 2 階層なため top-level コンストラクト と判定されていますが、さらに深い階層でDefaultを利用した場合、Defaultを除いたパスツリーで hash が計算されます。

通常コンストラクトの階層が違うとパスツリーが異なるため、hash 値が異なって論理 ID が衝突することはありません。Defaultを利用した場合パスツリーの計算時に無視されるので、コンストラクトのスコープ外だと思っているリソースと論理 ID が衝突する可能性があります。

下の例だと、末端の葉のリソースはParent/Default/MyQueueParent/MyQueue異なるパスツリーになりますが、hash 計算時にどちらもParent/MyQueueで計算されるため論理 ID が衝突します。

後述しますが、これを利用してコンストラクトツリーのリファクタリングが可能です。

Construct ID 命名時の注意点

ここからは、実際に Construct ID を命名する際に気をつけるべき点を見ていきます。

Construct ID はPascalCaseで記述する

Construct ID はPascalCaseで記述することで、リソース名の可読性を高め、意図せぬ 論理 ID 衝突を防ぐことができます。

⭕️ Good

new sqs.CfnQueue(this, "MyQueue");

❌ Bad

// 上位のConstruct IDと結合されて区切りがわかりづらくなる
new sqs.CfnQueue(this, "myQueue");

// 以下は論理IDが衝突する
new sqs.CfnQueue(this, "my-queue");
new sqs.CfnQueue(this, "myqueue");

Construct ID にConstructStackとつけない

Construct ID は Construct のスコープ内での一意性を担保するだけではなく、論理 ID と自動生成されるリソース名に使用されます。
Construct ID にConstructを含めてしまうと、CDK の世界で止めるべき事情が CloudFormation テンプレート や AWS の世界に漏れてしまいます。

Construct のクラス名と同じ名前を Construct ID につけたくなった場合、一歩踏みとどまって、論理 ID やリソース名に使われた時にどう見えるかを考えて命名しましょう。

StackのConstruct IDについても同様です。

⭕️ Good

new ApiConstruct(this, "Api");

new Feature1Stack(this, "Feature1")

❌ Bad

new ApiConstruct(this, "ApiConstruct");

new Feature1Stack(this, "Feature1Stack")

技術詳細を指定しすぎない

Construct ID に技術詳細を指定してしまうと、コンストラクトの中で変更があったときにConstruct IDも変更したくなります。
本番稼働後であれば論理 ID 変更によるリソース再作成を防ぐため、Construct IDを変更できない負債となってしまう可能性があります。

適切に抽象化しておくことで論理 ID の変更を防げます。

⭕️ Good

new ApiConstruct('Api')

❌ Bad

new ApiGatewaySqsLambdaConstruct('ApiGatewaySqsLambda')

親 コンストラクト で表している情報を繰り返さない

論理 ID は コンストラクト のパスツリーを結合して生成されます。
親 コンストラクト で表している情報を繰り返さないようにしましょう。

⭕️ Good

// ApiGatewayとRoleを定義するカスタムコンストラクト
class ApiConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    const role = new iam.Role(this, "ExecutionRole");

    // 省略
  }
}

// 親コンストラクト
class ParentConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    new ApiConstruct(this, "Api");
  }
}

IAM Role の論理 ID 生成用パスツリーはApi+ExecutionRoleApiExecutionRoleとなります。

❌ Bad

// ApiGatewayとRoleを定義するカスタムコンストラクト
class ApiConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    const role = new iam.Role(this, "ApiExecutionRole");

    // 省略
  }
}

// 親コンストラクト
class ParentConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    new ApiConstruct(this, "Api");
  }
}

IAM Role の論理 ID 生成用パスツリーはApi+ApiExecutionRoleApiApiExecutionRoleとなり、ApiApiが冗長です。

Resourceを使うべき場合

Resourceは、再利用するカスタムコンストラクトを作成する際に、論理 ID を短くするテクニックとして使用できます。

たとえば、共通する設定を事前に定義しておく以下のようなコンストラクト です。

export class LambdaConstruct extends Construct {
  readonly function: nodejs.NodejsFunction;

  constructor(scope: Construct, id: string, props: LambdaConstructProps) {
    super(scope, id);

    this.function = new nodejs.NodejsFunction(this, "Resource", {
      functionName,
      handler: "handler",
      tracing: lambda.Tracing.ACTIVE,
      runtime: lambda.Runtime.NODEJS_20_X,
      timeout: Duration.seconds(5),
      memorySize: 1769,
      architecture: lambda.Architecture.ARM_64,
      ...props,
    });
  }
}

このようなケースでは LambdaConstructを利用する側で、LambdaConstruct に対して関数の役割を示す Construct ID を指定することになります。

export class ParentConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);
    new LambdaConstruct(this, "CreateTodo");
    new LambdaConstruct(this, "GetTodo");
    new LambdaConstruct(this, "DeleteTodo");
  }
}

new ParentConstruct(this, "Parent");

カスタムコンストラクト 側でResourceを使用しているため、それぞれのパスツリーは以下になります。

  • Parent/CreateTodo/Resource
  • Parent/GetTodo/Resource
  • Parent/DeleteTodo/Resource

前述の通り、Resourceは論理 ID の可読部を生成する際に除去されるため、論理 ID を短くできます。

  • ParentCreateTodoXXXXXXXX
  • ParentGetTodoXXXXXXXX
  • ParentDeleteTodoXXXXXXXX

この理由から、多くの L2 コンストラクトの中でメインとなるリソースのConstruct ID はResourceが採用されています。
コンストラクトライブラリの開発者でなくても、Resourceを適切に使用することでリソース名の可読性を高めることができるため、覚えておいて損はないでしょう。

Defaultを使う場合

DefaultResourceと同様に論理 ID を短くするテクニックとして使えますが、前述した論理 ID 重複の可能性を意識しつつリファクタリングの用途での使用できます。
Defaultを利用したコンストラクトツリーの整理方法は以下のブログで紹介されています。

https://tmokmss.hatenablog.com/entry/20221212/1670804620

再利用を想定するカスタムコンストラクトは、そのコンストラクトからリソース移動などのリファクタリングを考慮する可能性は低く、論理 ID 重複が発生しづらいResourceで定義しておくことをオススメします。

機能単位やリソース単位など再利用を想定せずにコンストラクトを構造化する場合、将来的にリソース移動のリファクタリングが必要になる可能性があるため、Defaultで定義しておくのはいかがでしょうか。
たとえば、ApiConstructの中にApiGatewayと複数のLambdaを定義する際、ApiGatewayDefaultを指定しておくことで将来的に APIGateway をコンストラクトの外に引き上げることができます。

Construct ID は論理 ID と生成されるリソース名を考えて命名しましょう

CDK によって自動生成されるリソース名は、論理 ID が利用されつつも一定で打ち切られてランダムな文字列が付与されるため、一定の可読性が損なわれるのは仕方ないと諦めている節がありました。
こちらの記事により CDK で定義したリソースの命名の仕組みが理解できたので、それを踏まえた上で Construct ID はどう指定すべきなのか、自戒も込めてこの記事にまとめました。

Construct ID の命名方法が腹落ちしたので、今後は適切に指定して運用しやすいインフラを構築していきたいです。

最後に

  • 佐藤さん:素晴らしい記事を執筆いただき、ありがとうございました!
  • 岩田さん:Construct ID の指定について細かいテクニックを教えていただき、ありがとうございました!

この記事が誰かの役に立つと幸いです。
以上。リテールアプリ共創部のきんじょーでした。

参考

大変参考にさせていただきました。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.